总结几次面试中被问到的组件化问题 !
关注「技术最TOP」
,早上8:40不见不散!
作者:冬日毛毛雨
https://juejin.cn/post/7036298166907437070
阿里4轮面试,两轮面试都被问到组件化问题,面试的点各不相同,有组件化架构设计、插件化框架设计、路由架构设计、热修复设计等问题,但是最终都是殊途同归,所有的问题都汇集在这,如何对手机淘宝组架构设计?
一、组件化如何实现,组件化与插件化的差别在 哪里,该怎么选型
面试官:组件化如何实现,组件化与插件化的差别在哪里,该怎么选型**
心理分析:面试官从架构层次 了解求职者是否用过 模块化 组件化 和插件化,在过去经验有没有运用过这些技术到项目中,这道 题属于一个连环炮。求职者该格外小心
求职者:应该从 App 开发的需求来定义技术选型,分别说说模 块化,组件化 插件化的优势和区别
1.1 组件化
组件化,就是把 APP 拆分成不同功能模块,形成独立组件,让宿主调用。组件 化不一定是插件化,组件化是一个更大的概念:把模块解耦,组件之间代码不依 赖,宿主可以依赖组件;而插件化则具体到了技术点上,宿主通过 动态加载 来 调用组件,宿主不依赖组件,达到 完全解耦 的目的(比如图片缓存就可以看成 一个组件被多个 App 共用)。
适合于项目大 但是功能相对集中。比如 一个金融类的 App 里面只包含金融的 功能,金融功能又会有 借贷,理财,线下交易,把这些模块抽成单独的组件 二、插件化 Android 程序每次更新都要下载一个完整的 apk,而很多时候软件只是更新了一 个小功能而已,这样的话,就显得很麻烦。如果把 android 程序做成主程序+插 件化的形式呢,这样才利于小功能的扩展(比如一般 App 的皮肤样式就可以看 成一个插件)。
1.2插件化
Android 程序每次更新都要下载一个完整的 apk,而很多时候软件只是更新了一 个小功能而已,这样的话,就显得很麻烦。如果把 android 程序做成主程序+插 件化的形式呢,这样才利于小功能的扩展(比如一般 App 的皮肤样式就可以看 成一个插件)。
通过 gradle 配置的方式,将打 debug 包和 release 包分开。这 样会有一个好处,开发一个模块,在 debug 的时候,可以打成一 个 apk ,独立运行测试,可以完全独立于整个宿主 APP 的其他 所有组件;待到要打 release 包的时候,再把这个模块作为一个 library ,打成 aar ,作为整个宿主 APP 的一部分。而 debug 和 release 的切换都是通过 gradle 配置,可以做到无缝切换。至于 模块之间的跳转,可以用别名的方式,而不是用 Activity 和 Fragment 类名。这样所有的模块和宿主 APP 都是完全解耦的, 彻底解决了并行开发的可能造成的交叉依赖等问题
主要原理是:主要利用 Java ClassLoader
的原理,如 Android 的 DexClassLoader
,可动态加载的内容包括 apk、dex、jar 等。如下
插件化的优势:
适应并行开发,解耦各个模块,避免模块之间的交叉依赖,加快编译速度, 从而提高并行开发效率。 满足产品随时上线的需求 修复因为我们对自己要求不严格而写出来的 bug。 插件化的结果:分为稳定的 release 版本和不稳定的 snapshot 版本,每 个模块都高度解耦,没有交叉依赖,不会出现一个模块依赖了另一个模块, 其中一个人改了这个模块的代码,对另一个模块造成影响。
淘宝的框架是用了 osgi 的 bundle 概念,整个应用框架生命周期完整。适合于项目超级大 但是功能相对不集中。比如 一个支付宝 App 里面即包 含共享单车 也包含 电影票。这种与本业务完全不同的 可以做成插件的形式 插件化弊端:每一个插件都是一个 apk,插件多的时候管理起来也麻烦。
二、说下组件之间的跳转和组件通信原理机制
面试官: 说下组件之间的跳转和组件通信原理机制
心理分析:面试官从架构层次 了解求职者是否对组件化有深入研 究。是否使用过组件化,使用有多深。通过该问题一目了然。如果 能说出项目的演进 组件通信选型 绝对是一个加分项
求职者:应该从为什么会用到组件化 和组件定义,组件通信的 演进说起
我们公司的一个单体项目进行组件化架构改造,我们最开始从以下 7 个方面入手:
代码解耦。如何将一个庞大的工程分成有机的整体? 组件单独运行。因为每个组件都是高度内聚的,是一个完整的整体,如何 让其单独运行和调试? 组件间通信。由于每个组件具体实现细节都互相不了解,但每个组件都需 要给其他调用方提供服务,那么主项目与组件、组件与组件之间如何通信 就变成关键? UI 跳转。UI 跳转指的是特殊的数据传递,跟组件间通信区别有什么不 同? 组件生命周期。这里的生命周期指的是组件在应用中存在的时间,组件是 否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等管理问题。 集成调试。在开发阶段如何做到按需编译组件?一次调试中可能有一两个 组件参与集成,这样编译时间就会大大降低,提高开发效率。 代码隔离。组件之间的交互如果还是直接引用的话,那么组件之间根本没 有做到解耦,如何从根本上避免组件之间的直接引用,也就是如何从根本 上杜绝耦合的产生?
今天则会从更小细粒度入手,主要讲讲在组件化架构下组件与组件之间通信机制 是如何、包括所谓的 UI 跳转,其实也是组件化通信,只不过它稍微特殊点,单 独抽取出来而已。学习知识的过程很常见的一个思路就是从整体概况入手,首先 对整体有个粗略的印象,然后再深入细节,抽丝剥茧般去挖掘其中的内在原理, 一个点一个不断去突破,这样就能建立起自己整个知识树,所以今天我们就从通 信机制这个点入手,看看其中内在玄机有哪些。
2.1 思维导图
同样,在每写一篇文章之前,放个思维导图,这样做的好处对于想写的内容有很 好的梳理,逻辑和结构上显得清晰点。
总所周知,Android 提供了很多不同的信息的传递方式,比如在四大组件中本地 广播、进程间的 AIDL、匿名间的内存共享、Intent Bundle 传递等等,那么在这 么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。
本地广播,也就是
LoacalBroadcastRecevier
。更多是用在同一个应用内的不同系 统规定的组件进行通信,好处在于:发送的广播只会在自己的 APP 内传播,不 会泄漏给其他的 APP,其他 APP 无法向自己的 APP 发送广播,不用被其他 APP 干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制 全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组 件化通信过程中采用比例不高。进程间的
AIDL
。这个粒度在于进程,而我们组件化通信过程往往是在线程中, 况且AIDL
通信也是属于系统级通信,底层以Binder
机制,虽说 Android 提供模 板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组 件化通信过程中。匿名的内存共享。比如用
Sharedpreferences
,在处于多线程场景下,往往会线 程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等 等。Intent Bundle
传递。包括显性和隐性传递,显性传递需要明确包名路径,组件 与组件往往是需要互相依赖,这背离组件化中 SOP(关注点分离原则),如果走 隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错, 排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其 他组件打交道则使用场景不多。
说了这么多,那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独 立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无 法传递消息,那要如何才能完成这种交流?
目前主流做法之一就是引入第三者,比如图中的 Base Module。组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编 译在 APP Module 中,那时将不会有这种隔阂,那么其中的 Base Module 就是 跨越组件化层级的关键,也是模块间信息交流的基础。比较有代表性的组件化开 源框架有得到 DDComponentForAndroid
、阿里 Arouter
、聚美 Router
等等。
2.2 事件总线
除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种 方式目前开源的框架中使用比例不高,如图:
事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活 中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租 信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因 为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象 成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上 贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之 前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产 生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进 行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。
在目前开源库中,有 EventBus
、RxBus
就是采用这种发布/订阅模式,优点是简 化了 Android 组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态 设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件 类太多,无形中加大了维护成本。那么在组件化开源框架中有 ModuleBus
、CC
等 等。
事件总线,又可以叫做组件总线,以 ModuleBus 框架的源码为例,这个方案特别之处在于其借鉴了 EventBus 的思想,组件的注册/注销和组件调用的事件发送都跟 EventBus 类似,能够传递一些 基础类型的数据,而并不需要在 Base Moudel 中添加额外的类。所以不会影响 Base 模块的架构,但是无法动态移除信息接收端的代码,而自定义的事件信息 类型还是需要添加到 Base Module 中才能让其他功能模块索引。
其中的核心代码是在与 ModuleBus 类,其内部维护了两个 ArrayMap 键对值列 表,如下:
private static ArrayMap<Object,ArrayMap<String,MethodInfo>>
moduleEventMethods = new ArrayMap<>();
private static ArrayMap<Class<?>,ArrayMap<String,ArrayList<Object>>>
moduleMethodClient = new ArrayMap<>()
在使用方法上,在 onCreate()和 onDestroy()中需要注册和解绑,比如
ModuleBus.getInstance().register(this);
ModuleBus.getInstance().unregister(this);
最终使用类似 EventBus 中 post 方法一样,进行两个组件间的通信。这个框架 的封装的 post 方法如下
public void post(Class<?> clientClass,String methodName,Object...args){
if(clientClass == null || methodName == null ||methodName.length() == 0) return;
ArrayList<Object> clientList = getClient(clientClass,methodName)
for(Object c: clientList){ ArrayMap<String,MethodInfo> methods = moduleEventMethods.get(c);
Method method = methods.get(methodName).m;
method.invoke(c,args);
}
可以看到,它是通过遍历之前内部的 ArrayMap
,把注册在里面的方法找出,根据传入的参数进行匹配,使用反射调用。
2.3 接口+路由
相对于事件总线的方式,组件间通信更多使用的还是基于 Base Module 的接口+路由的方式
接口+路由实现方式则相对容易理解点,我之前实践的一个项目就是通过这种方式实现的。实现思路是专门抽取一个 LibModule
作为路由服务,每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向 Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。比如定义两个路由地址,一个登陆组件,一个设置组件,核心代码:
public class RouterPath {
public static final String ROUTER_PATH_TO_LOGIN_SERVICE = "/login/service";
public static final String ROUTER_PATH_TO_SETTING_SERVICE = "/setting/service"; }
那么就相应着就有两个接口 API,如下:
public interface ILoginProvider extends IProvider {
void goToLogin(Activity activity);
}
public interface ISettingProvider extends IProvider {
void goToSetting(Activity activity);
}
}
这两个接口 API 对应着是向外暴露这两个组件的能提供的通信能力,然后每个组 件对接口进行实现,如下:
@Override
public void init(Context context) {
}
@Override
public void goToLogin(Activity activity) {
Intent loginIntent = new Intent(activity, LoginActivity.class);
activity.startActivity(loginIntent);
}
}
这其中使用的到了阿里的 ARouter 页面跳转方式,内部本质也是接口+实现方式 进行组件间通信。调用则很简单了,如下:
ILoginProvider loginService = (ILoginProvider)
ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).naviga tion();
if(loginService != null){
loginService.goToLogin(MainActivity.this);
}
还有一个组件化框架,就是 ModularizationArchitecture ,它本质实现方式也是 接口+实现,但是封装形式稍微不一样点,它是每个功能模块中需要使用注解建立 Action
事件,每个 Action
完成一个事件动作。invoke 只是方法名为反射,并 未用到反射,而是使用接口方式调用,参数是通过 HashMap
传递的,无法传递 对象。具体详解可以看这篇文章 Android 架构思考(模块化、多进程)。
2.4 页面跳转
页面跳转也算是一种组件间的通信,只不过它相对粒度更细化点,之前我们描述 的组件间通信粒度会更抽象点,页面跳转则是定位到某个组件的某个页面,可能 是某个 Activity,或者某个 Fragment,要跳转到另外一个组件的 Activity 或 Fragment,是这两者之间的通信。甚至在一般没有进行组件化架构的工程项目 中,往往也会封装页面之间的跳转代码类,往往也会有路由中心的概念。不过一 般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。
每个组件可以注册自己所能处理的短链的 Scheme
和 Host
,并定义传输数据的 格式,然后注册到统一的 UIRouter
中,UIRouter
通过 Scheme
和 Host
的匹 配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注 解,然后通过 APT 形成具体的逻辑代码。下面简单介绍目前比较主流的两个框架核心实现思路:
ARouter
ARouter 核心实现思路是,我们在代码里加入的 @Route
注解,会在编译时期通 过 apt 生成一些存储 path
和 activityClass
映射关系的类文件,然后 app 进程启 动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在 map 里),然后在进行路由跳转的时候,通过 build()方法传入要到达页面的路由 地址,ARouter 会通过它自己存储的路由表找到路由地址对应的 Activity.class(activity.class = map.get(path))
,然后 new Intent()
,当调用 ARouter 的 withString()
方法它的内部会调用 intent.putExtra(String name, String value)
, 调用 navigation()
方法,它的内部会调用 startActivity(intent)
进行跳转,这样便可 以实现两个相互没有依赖的 module 顺利的启动对方的 Activity 了。
ActivityRouter
核心实现思路是,它是通过路由 + 静态方法来实现,在静态方 法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle)
,基 于 apt 技术,通过注解方式来实现 URL 打开 Activity 功能,并支持在 WebView 和外部浏览器使用,支持多级 Activity 跳转,支持 Bundle、Uri 参数注入并转换 参数类型。它实现相对简单点,也是比较早期比较流行的做法,不过学习它也是 很有参考意义的。
小结
总的来说,组件间的通信机制在组件化编程和组件化架构中是很重要的一个环 节,可能在每个组件独自开发阶段,不需要与其他组件进行通信,只需要在内部 通信即可,当处于组件集成阶段,那就需要大量组件进行互相通信,体现在每个 业务互相协作,如果组件间设计的不好,打开一个页面或调用一个方法,想当耗 时或响应慢,那么体现的则是这个 APP 使用比较卡顿,仅仅打开一个页面就是 需要好几秒才能打开,则严重影响使用者的体验了,甚至一些大型 APP,可能组 件分化更小,种类更多,那么组件间的通信则至关重要了。所以,要打造一个良好的组件化框架,如何设计一个更适合自己本身的业务类型的通信机制,就需要多多进行思考了。
---END---